package gitfs

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"path/filepath"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/hanwen/go-fuse/v2/fs"
	"github.com/hanwen/go-fuse/v2/fuse"
	"gopkg.in/src-d/go-git.v4"
	"gopkg.in/src-d/go-git.v4/plumbing"
	"gopkg.in/src-d/go-git.v4/plumbing/filemode"
	"gopkg.in/src-d/go-git.v4/plumbing/object"
	"gopkg.in/src-d/go-git.v4/utils/ioutil"
)

type gRoot struct {
	fs.Inode

	r *git.Repository
}

var _ = (fs.NodeOnAdder)((*gRoot)(nil))

func (gr *gRoot) OnAdd(ctx context.Context) {
	ref, err := gr.r.Head()
	if err != nil {
		fmt.Println(err)
		return
	}
	commit, err := gr.r.CommitObject(ref.Hash())
	if err != nil {
		fmt.Println(err)
		return
	}
	tree, err := gr.r.TreeObject(commit.TreeHash)
	if err != nil {
		fmt.Println(err)
		return
	}
	seen := make(map[plumbing.Hash]bool)
	walker := object.NewTreeWalker(tree, true, seen)
	for {
		name, entry, err := walker.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			break
		}
		if entry.Mode == filemode.Dir {
			continue
		}
		dir, base := filepath.Split(filepath.Clean(name))

		p := &gr.Inode
		for _, component := range strings.Split(dir, "/") {

			if len(component) == 0 {
				continue
			}
			ch := p.GetChild(component)
			if ch == nil {
				in := fs.Inode{}
				ch = p.NewPersistentInode(ctx, &in,
					fs.StableAttr{Mode: fuse.S_IFDIR})
				p.AddChild(component, ch, true)
			}
			p = ch
		}
		file, err := tree.TreeEntryFile(&entry)
		ch := p.NewPersistentInode(ctx, &gFile{file: file}, fs.StableAttr{})
		p.AddChild(base, ch, true)
	}
}

func NewGitTree(directory string) (fs.InodeEmbedder, error) {
	r, err := git.PlainOpen(directory)
	if err != nil {
		return nil, err
	}

	return &gRoot{r: r}, nil
}

type gFile struct {
	fs.Inode
	file *object.File

	mu   sync.Mutex
	data []byte
}

var _ = (fs.NodeOpener)((*gFile)(nil))
var _ = (fs.NodeGetattrer)((*gFile)(nil))

// Getattr sets the minimum, which is the size. A more full-featured
// FS would also set timestamps and permissions.
func (gf *gFile) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {

	out.Mode = uint32(gf.file.Mode) & 07777
	out.Nlink = 1
	out.Mtime = uint64(time.Now().Unix())
	out.Atime = out.Mtime
	out.Ctime = out.Mtime
	out.Size = uint64(gf.file.Size)
	const bs = 512
	out.Blksize = bs
	out.Blocks = (out.Size + bs - 1) / bs
	return 0
}

// Open lazily unpacks zip data
func (gf *gFile) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) {
	gf.mu.Lock()
	defer gf.mu.Unlock()
	if gf.data == nil {
		reader, err := gf.file.Reader()
		if err != nil {
			return nil, fuse.FOPEN_KEEP_CACHE, 0
		}
		defer ioutil.CheckClose(reader, &err)

		buf := new(bytes.Buffer)
		if _, err := buf.ReadFrom(reader); err != nil {
			return nil, fuse.FOPEN_KEEP_CACHE, 0
		}
		gf.data = buf.Bytes()
	}

	// We don't return a filehandle since we don't really need
	// one.  The file content is immutable, so hint the kernel to
	// cache the data.
	return nil, fuse.FOPEN_KEEP_CACHE, 0
}

// Read simply returns the data that was already unpacked in the Open call
func (gf *gFile) Read(ctx context.Context, f fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
	end := int(off) + len(dest)
	if end > len(gf.data) {
		end = len(gf.data)
	}
	return fuse.ReadResultData(gf.data[off:end]), 0
}
